
// $Id$

package net.sf.persist.tests.framework;

import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;

import java.sql.Connection;
import java.sql.SQLException;
import java.util.List;
import java.util.Map;

import net.sf.persist.Persist;
import net.sf.persist.Result;
import net.sf.persist.TableMapping;
import net.sf.persist.tests.common.Simple;
import net.sf.persist.tests.common.TestSimple;

public class BeanTest {
	
	// TODO: test updates passing a single value
	
	public static void test(Persist persist, BeanMap beanMap) {
		Connection connection = persist.getConnection();
		Class cls = DynamicBean.createBeanClass(beanMap, false);
		Class clsNoTable = DynamicBean.createBeanClass(beanMap, true);
		
		String tableName = dbName(beanMap.getClassName());
		
		try {
			connection.createStatement().execute("delete from " + tableName);
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
		BeanTest.testAll(persist, cls, clsNoTable, beanMap);	
		

		try {
			connection.createStatement().execute("delete from " + tableName);
		} catch (SQLException e) {
			throw new RuntimeException(e);
		}
		BeanTest.testAllNull(persist, cls, clsNoTable, beanMap);
		
		if (beanMap.supportsAutoGeneratedKeys()) {
			testExecuteUpdateAutoGeneratedKeys(persist);
			testSetAutoGeneratedKeys(persist);
		}

	}
	
	/**
	 * Perform tests using a bean with non-null values
	 */
	public static void testAll(Persist persist, Class cls, Class clsNoTable, BeanMap beanMap) {
		persist.setAutoCommit(false);
		Object obj = DynamicBean.createInstance(cls, beanMap, false);
		BeanTest.testInsert(persist, obj, beanMap);
		BeanTest.testSelectByFields(persist, obj, beanMap);
		BeanTest.testSelectFields(persist, obj, beanMap, false);
		BeanTest.testSelectMap(persist, obj, beanMap);
		BeanTest.testReadNoTable(persist, obj, clsNoTable);
		persist.commit();
		persist.setAutoCommit(true);
	}

	/**
	 * Perform tests using a bean with null values
	 */
	public static void testAllNull(Persist persist, Class cls, Class clsNoTable, BeanMap beanMap) {
		persist.setAutoCommit(false);
		Object objNull = DynamicBean.createInstance(cls, beanMap, true);
		BeanTest.testInsert(persist, objNull, beanMap);
		BeanTest.testSelectByFieldsNull(persist, objNull, beanMap);
		BeanTest.testSelectFields(persist, objNull, beanMap, true);
		BeanTest.testSelectMap(persist, objNull, beanMap);
		BeanTest.testReadNoTable(persist, objNull, clsNoTable);
		persist.commit();
		persist.setAutoCommit(true);
	}
	
	/**
	 * tests insertion of a bean
	 */
	public static void testInsert(Persist persist, Object obj, BeanMap beanMap) {
		Class cls = obj.getClass();
		String tableName = dbName(obj.getClass().getSimpleName());
		
		// perform insert
		persist.insert(obj);
		
		// check if a single result exists in the table (expects the table to be clean by the beginning of the process)
		String sql = "select * from " + tableName;
		List read = persist.readList(cls, sql);
		if (read.size()!=1) {
			throw new AssertionError("Expected 1 result but got [" + read.size() + "] as result of sql [" + sql + "]");
		}
		
		// check if the bean read is the same as inserted
		if (!obj.equals(read.get(0))) { 
			throw new AssertionError("Expected [" + DynamicBean.toString(obj) + "] but got [" 
					+ DynamicBean.toString(read.get(0)) + "] as result of [" + sql + "]");
		}
	}
	
	/**
	 * tests reading with NoTable class
	 */
	public static void testReadNoTable(Persist persist, Object obj, Class clsNoTable) {
		
		String tableName = dbName(obj.getClass().getSimpleName());
		
		// now read with NoTable (assumes previously inserted data)
		Object objNoTable = persist.read(clsNoTable, "select * from " + tableName);
		
		// compare values
		if (!DynamicBean.compareBeansFromDifferentClasses(obj, objNoTable)) {
			throw new AssertionError("Expected [" + DynamicBean.toString(obj) + "] but got [" 
					+ DynamicBean.toString(objNoTable) + "]");
		}
	}	
		
	/**
	 * For each field and each field type, execute a query in the format
	 * select * from tableName where columnName=?
	 */
	public static void testSelectByFields(Persist persist, Object obj, BeanMap beanMap) {
		Class cls = obj.getClass();
		String tableName = dbName(obj.getClass().getSimpleName());
		
		// for each field in the bean
		for (FieldMap fieldMap : beanMap.getFields()) {
			
			// only perform tests if the field supports queries by value (blobs in oracle, for instance, don't)
			if (fieldMap.isSupportsQueryByValue()) {
				String columnName = dbName(fieldMap.getFieldName());
				String sql = "select * from " + tableName + " where " + columnName + "=?";
				Object fieldValue = DynamicBean.getFieldValue(obj, fieldMap.getFieldName());
				
				// for each type supported by the field, test if a query using an object of that type returns data correctly
				for (Class fieldType : fieldMap.getTypes()) {
					
					// use value converted to the type being tested
					Object fieldValueConverted = DynamicBean.convertToType(fieldType, fieldValue);
					
					// query using the type being tested
					Object ret = persist.read(cls, sql, fieldValueConverted);
					
					// check if the result is not null and has the same data as the object being tested
					if (ret==null) {
						throw new AssertionError("Expected not null value but got null as result of [" + sql 
								+ "] with parameter [" + fieldValue + "]");
					}
					if (!obj.equals(ret)) {
						throw new AssertionError("Expected [" + DynamicBean.toString(obj) + "] but got [" 
								+ DynamicBean.toString(ret) + "] as result of [" + sql + "]");
					}
				}
			}
		}
	}
	
	/**
	 * For each field, execute a query in the format
	 * select * from tableName where columnName is null
	 */
	public static void testSelectByFieldsNull(Persist persist, Object obj, BeanMap beanMap) {
		Class cls = obj.getClass();
		String tableName = dbName(obj.getClass().getSimpleName());
		
		// for each field in the bean
		for (FieldMap fieldMap : beanMap.getFields()) {
			
			// only perform tests if the field supports queries by value (blobs in oracle, for instance, don't)
			if (fieldMap.isSupportsQueryByValue()) {
				String columnName = dbName(fieldMap.getFieldName());
				
				// test if query for null value in the column related with the field return the object correctly
				String sql = "select * from " + tableName + " where " + columnName + " is null";
				Object ret = persist.read(cls, sql);
				if (ret==null) throw new AssertionError("Expected not null value but got null as result of [" + sql + "]");
				if (!obj.equals(ret)) {
					throw new AssertionError("Expected [" + DynamicBean.toString(obj) + "] but got [" 
							+ DynamicBean.toString(ret) + "] as result of [" + sql + "]");
				}
			}
		}
	}
	
	/**
	 * for each field, and for each field type perform a query in the form
	 * select columnName from tableName
	 */
	public static void testSelectFields(Persist persist, Object obj, BeanMap beanMap, boolean useNulls) {
		String tableName = dbName(obj.getClass().getSimpleName());
		
		// for each field in the bean
		for (FieldMap fieldMap : beanMap.getFields()) {
			String columnName = dbName(fieldMap.getFieldName());
			String sql = "select " + columnName + " from " + tableName;
			
			// get field value from the bean
			Object fieldValue = DynamicBean.getFieldValue(obj, fieldMap.getFieldName());
			
			// for each supported type
			for (Class fieldType : fieldMap.getTypes()) {
				
				// query for a single column data using the field type (eg byte, Byte, String, InputStream, etc.)
				Object ret = persist.read(fieldType, sql);
				
				if (useNulls) {
					// check if "null", which means 0 for numeric values type as primitive (byte, short, int, etc.) 
					if (!DynamicBean.isNull(fieldType, ret)) throw new AssertionError("Expected null value but got [" + ret 
							+ "] as result of [" + sql + "]");
				}
				else {
					if (ret==null) {
						throw new AssertionError("Expected not null value but got null as result of [" + sql + "]");
					}
					
					// TODO: maybe test compatibility of return type with field type?				
					
					// compare values using a method that takes into consideration "compatible" types 
					// (eg char[]-String, double-BigDecimal, etc)
					Object retConverted = DynamicBean.convertToType(fieldValue.getClass(), ret);
					if (!DynamicBean.compareValues(fieldValue, retConverted)) {
						throw new AssertionError("Expected [" + fieldValue + "] but got [" + ret + "] as result of [" + sql + "]");
					}
				}
			}
		}
	}
	
	/**
	 * perform [select * from tableName] and get the results as a map
	 */
	public static void testSelectMap(Persist persist, Object obj, BeanMap beanMap) {
		String tableName = dbName(obj.getClass().getSimpleName());
		
		// read list of all data in the table as a map
		String sql = "select * from " + tableName;
		List<Map<String,Object>> mapList = persist.readMapList(sql);
		
		// asserts there's only one entry (added during the insert test)
		if (mapList.size()!=1) {
			throw new AssertionError("Expected 1 result but got [" + mapList.size() + "] as result of sql [" + sql + "]");
		}
		
		// use the first (or single) map returned
		Map m = mapList.get(0);
		
		// for each field in the bean
		for (FieldMap fieldMap : beanMap.getFields()) {
			String columnName = dbName(fieldMap.getFieldName());
			
			// get the field value
			Object fieldValue = DynamicBean.getFieldValue(obj, fieldMap.getFieldName());
			
			// get the corresponding map value
			Object mapValue = m.get(columnName);
			
			// if field supports comparisons on the map values (mysql's year2 and year4, for instance, 
			// have Date objects here, but must be inserted as short's, therefore can't be compared properly)
			if (fieldMap.isSupportsCompareMapValue()) {
				
				// compare values using a method that takes into consideration "compatible" types 
				// (eg char[]-String, double-BigDecimal, etc) 
				if (!DynamicBean.compareValues(fieldValue, mapValue)) {
					throw new AssertionError("Map entry [" + columnName + "]=[" + mapValue + "] does not match field [" 
							+ fieldMap.getFieldName() + "]=[" + fieldValue + "] as result of sql [" + sql + "]");
				}
			}
		}
	}
	
	/**
	 * Tests if explicitly specifying autoGeneratedKeys in executeUpdate works
	 */
	public static void testExecuteUpdateAutoGeneratedKeys(Persist persist) {
		
		TableMapping mapping = (TableMapping) persist.getMapping(Simple.class);
		if (mapping.supportsGetGeneratedKeys()) {
			
			// some data to insert
			int intCol = DynamicBean.randomInt(0,Integer.MAX_VALUE/2);
			String stringCol = DynamicBean.randomString(255);
			
			// insert with explicit auto generated keys and check result object data
			String[] autoGeneratedKeys = new String[] { "id" };
			Result result = persist.executeUpdate(Simple.class, "insert into simple (int_col,string_col) values(?,?)", 
					autoGeneratedKeys, intCol, stringCol);
			assertEquals(1, result.getGeneratedKeys().size());
			assertEquals(1, result.getRowsModified());
			
			// read object and compare with inserted data
			Simple simpleRead = persist.read(Simple.class, "select * from simple where int_col=? and string_col=?", 
					intCol, stringCol);
			assertNotNull(simpleRead);
			assertEquals(intCol, simpleRead.getIntCol());
			assertEquals(stringCol, simpleRead.getStringCol());
			
			// delete object and check it was removed
			persist.delete(simpleRead);
			simpleRead = persist.read(Simple.class, "select * from simple where int_col=? and string_col=?", intCol, stringCol);
			assertNull(simpleRead);			
		}

	}
	
	/**
	 * Tests setUpdateGeneratedKeys
	 */
	public static void testSetAutoGeneratedKeys(Persist persist) {

		TableMapping mapping = (TableMapping) persist.getMapping(Simple.class);
		if (mapping.supportsGetGeneratedKeys()) {
		
			persist.setUpdateAutoGeneratedKeys(true);
			
			// insert object with setUpdateAutoGeneratedKeys option
			Simple simpleInsert = TestSimple.buildSimple();
			simpleInsert.setId(0);
			persist.insert(simpleInsert);
			assertTrue(0!=simpleInsert.getId());
			
			// read object using primary key (auto generated)
			Simple simpleRead = persist.read(Simple.class, "select * from simple where id=?", simpleInsert.getId());
			assertEquals(simpleInsert, simpleRead);		
			
			// delete object by primary key and check it was removed
			persist.delete(simpleRead);
			simpleRead = persist.readByPrimaryKey(Simple.class, simpleRead.getId());
			assertNull(simpleRead);
			
			persist.setUpdateAutoGeneratedKeys(false);
			
		}
	}
	
	// ---------- helpers ----------
	
	/**
	 * Returned the database table/column name related with a given bean/field name
	 */
	private static String dbName(String s) {
		String name = s.replaceAll("([A-Z])", "_$1").toLowerCase();
		return name.charAt(0)=='_' ? name.substring(1) : name;
	}

}
